/*
 * Copyright (c) 1998, 2013, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package javax.swing.text;

import java.io.Writer;
import java.io.IOException;
import java.util.Enumeration;

/**
 * AbstractWriter is an abstract class that actually
 * does the work of writing out the element tree
 * including the attributes.  In terms of how much is
 * written out per line, the writer defaults to 100.
 * But this value can be set by subclasses.
 *
 * @author Sunita Mani
 */

public abstract class AbstractWriter {

  private ElementIterator it;
  private Writer out;
  private int indentLevel = 0;
  private int indentSpace = 2;
  private Document doc = null;
  private int maxLineLength = 100;
  private int currLength = 0;
  private int startOffset = 0;
  private int endOffset = 0;
  // If (indentLevel * indentSpace) becomes >= maxLineLength, this will
  // get incremened instead of indentLevel to avoid indenting going greater
  // than line length.
  private int offsetIndent = 0;

  /**
   * String used for end of line. If the Document has the property
   * EndOfLineStringProperty, it will be used for newlines. Otherwise
   * the System property line.separator will be used. The line separator
   * can also be set.
   */
  private String lineSeparator;

  /**
   * True indicates that when writing, the line can be split, false
   * indicates that even if the line is > than max line length it should
   * not be split.
   */
  private boolean canWrapLines;

  /**
   * True while the current line is empty. This will remain true after
   * indenting.
   */
  private boolean isLineEmpty;

  /**
   * Used when indenting. Will contain the spaces.
   */
  private char[] indentChars;

  /**
   * Used when writing out a string.
   */
  private char[] tempChars;

  /**
   * This is used in <code>writeLineSeparator</code> instead of
   * tempChars. If tempChars were used it would mean write couldn't invoke
   * <code>writeLineSeparator</code> as it might have been passed
   * tempChars.
   */
  private char[] newlineChars;

  /**
   * Used for writing text.
   */
  private Segment segment;

  /**
   * How the text packages models newlines.
   *
   * @see #getLineSeparator
   */
  protected static final char NEWLINE = '\n';


  /**
   * Creates a new AbstractWriter.
   * Initializes the ElementIterator with the default
   * root of the document.
   *
   * @param w a Writer.
   * @param doc a Document
   */
  protected AbstractWriter(Writer w, Document doc) {
    this(w, doc, 0, doc.getLength());
  }

  /**
   * Creates a new AbstractWriter.
   * Initializes the ElementIterator with the
   * element passed in.
   *
   * @param w a Writer
   * @param doc an Element
   * @param pos The location in the document to fetch the content.
   * @param len The amount to write out.
   */
  protected AbstractWriter(Writer w, Document doc, int pos, int len) {
    this.doc = doc;
    it = new ElementIterator(doc.getDefaultRootElement());
    out = w;
    startOffset = pos;
    endOffset = pos + len;
    Object docNewline = doc.getProperty(DefaultEditorKit.
        EndOfLineStringProperty);
    if (docNewline instanceof String) {
      setLineSeparator((String) docNewline);
    } else {
      String newline = null;
      try {
        newline = System.getProperty("line.separator");
      } catch (SecurityException se) {
      }
      if (newline == null) {
        // Should not get here, but if we do it means we could not
        // find a newline string, use \n in this case.
        newline = "\n";
      }
      setLineSeparator(newline);
    }
    canWrapLines = true;
  }

  /**
   * Creates a new AbstractWriter.
   * Initializes the ElementIterator with the
   * element passed in.
   *
   * @param w a Writer
   * @param root an Element
   */
  protected AbstractWriter(Writer w, Element root) {
    this(w, root, 0, root.getEndOffset());
  }

  /**
   * Creates a new AbstractWriter.
   * Initializes the ElementIterator with the
   * element passed in.
   *
   * @param w a Writer
   * @param root an Element
   * @param pos The location in the document to fetch the content.
   * @param len The amount to write out.
   */
  protected AbstractWriter(Writer w, Element root, int pos, int len) {
    this.doc = root.getDocument();
    it = new ElementIterator(root);
    out = w;
    startOffset = pos;
    endOffset = pos + len;
    canWrapLines = true;
  }

  /**
   * Returns the first offset to be output.
   *
   * @since 1.3
   */
  public int getStartOffset() {
    return startOffset;
  }

  /**
   * Returns the last offset to be output.
   *
   * @since 1.3
   */
  public int getEndOffset() {
    return endOffset;
  }

  /**
   * Fetches the ElementIterator.
   *
   * @return the ElementIterator.
   */
  protected ElementIterator getElementIterator() {
    return it;
  }

  /**
   * Returns the Writer that is used to output the content.
   *
   * @since 1.3
   */
  protected Writer getWriter() {
    return out;
  }

  /**
   * Fetches the document.
   *
   * @return the Document.
   */
  protected Document getDocument() {
    return doc;
  }

  /**
   * This method determines whether the current element
   * is in the range specified.  When no range is specified,
   * the range is initialized to be the entire document.
   * inRange() returns true if the range specified intersects
   * with the element's range.
   *
   * @param next an Element.
   * @return boolean that indicates whether the element is in the range.
   */
  protected boolean inRange(Element next) {
    int startOffset = getStartOffset();
    int endOffset = getEndOffset();
    if ((next.getStartOffset() >= startOffset &&
        next.getStartOffset() < endOffset) ||
        (startOffset >= next.getStartOffset() &&
            startOffset < next.getEndOffset())) {
      return true;
    }
    return false;
  }

  /**
   * This abstract method needs to be implemented
   * by subclasses.  Its responsibility is to
   * iterate over the elements and use the write()
   * methods to generate output in the desired format.
   */
  abstract protected void write() throws IOException, BadLocationException;

  /**
   * Returns the text associated with the element.
   * The assumption here is that the element is a
   * leaf element.  Throws a BadLocationException
   * when encountered.
   *
   * @param elem an <code>Element</code>
   * @return the text as a <code>String</code>
   * @throws BadLocationException if pos represents an invalid location within the document
   */
  protected String getText(Element elem) throws BadLocationException {
    return doc.getText(elem.getStartOffset(),
        elem.getEndOffset() - elem.getStartOffset());
  }


  /**
   * Writes out text.  If a range is specified when the constructor
   * is invoked, then only the appropriate range of text is written
   * out.
   *
   * @param elem an Element.
   * @throws IOException on any I/O error
   * @throws BadLocationException if pos represents an invalid location within the document.
   */
  protected void text(Element elem) throws BadLocationException,
      IOException {
    int start = Math.max(getStartOffset(), elem.getStartOffset());
    int end = Math.min(getEndOffset(), elem.getEndOffset());
    if (start < end) {
      if (segment == null) {
        segment = new Segment();
      }
      getDocument().getText(start, end - start, segment);
      if (segment.count > 0) {
        write(segment.array, segment.offset, segment.count);
      }
    }
  }

  /**
   * Enables subclasses to set the number of characters they
   * want written per line.   The default is 100.
   *
   * @param l the maximum line length.
   */
  protected void setLineLength(int l) {
    maxLineLength = l;
  }

  /**
   * Returns the maximum line length.
   *
   * @since 1.3
   */
  protected int getLineLength() {
    return maxLineLength;
  }

  /**
   * Sets the current line length.
   *
   * @since 1.3
   */
  protected void setCurrentLineLength(int length) {
    currLength = length;
    isLineEmpty = (currLength == 0);
  }

  /**
   * Returns the current line length.
   *
   * @since 1.3
   */
  protected int getCurrentLineLength() {
    return currLength;
  }

  /**
   * Returns true if the current line should be considered empty. This
   * is true when <code>getCurrentLineLength</code> == 0 ||
   * <code>indent</code> has been invoked on an empty line.
   *
   * @since 1.3
   */
  protected boolean isLineEmpty() {
    return isLineEmpty;
  }

  /**
   * Sets whether or not lines can be wrapped. This can be toggled
   * during the writing of lines. For example, outputting HTML might
   * set this to false when outputting a quoted string.
   *
   * @since 1.3
   */
  protected void setCanWrapLines(boolean newValue) {
    canWrapLines = newValue;
  }

  /**
   * Returns whether or not the lines can be wrapped. If this is false
   * no lineSeparator's will be output.
   *
   * @since 1.3
   */
  protected boolean getCanWrapLines() {
    return canWrapLines;
  }

  /**
   * Enables subclasses to specify how many spaces an indent
   * maps to. When indentation takes place, the indent level
   * is multiplied by this mapping.  The default is 2.
   *
   * @param space an int representing the space to indent mapping.
   */
  protected void setIndentSpace(int space) {
    indentSpace = space;
  }

  /**
   * Returns the amount of space to indent.
   *
   * @since 1.3
   */
  protected int getIndentSpace() {
    return indentSpace;
  }

  /**
   * Sets the String used to represent newlines. This is initialized
   * in the constructor from either the Document, or the System property
   * line.separator.
   *
   * @since 1.3
   */
  public void setLineSeparator(String value) {
    lineSeparator = value;
  }

  /**
   * Returns the string used to represent newlines.
   *
   * @since 1.3
   */
  public String getLineSeparator() {
    return lineSeparator;
  }

  /**
   * Increments the indent level. If indenting would cause
   * <code>getIndentSpace()</code> *<code>getIndentLevel()</code> to be &gt;
   * than <code>getLineLength()</code> this will not cause an indent.
   */
  protected void incrIndent() {
    // Only increment to a certain point.
    if (offsetIndent > 0) {
      offsetIndent++;
    } else {
      if (++indentLevel * getIndentSpace() >= getLineLength()) {
        offsetIndent++;
        --indentLevel;
      }
    }
  }

  /**
   * Decrements the indent level.
   */
  protected void decrIndent() {
    if (offsetIndent > 0) {
      --offsetIndent;
    } else {
      indentLevel--;
    }
  }

  /**
   * Returns the current indentation level. That is, the number of times
   * <code>incrIndent</code> has been invoked minus the number of times
   * <code>decrIndent</code> has been invoked.
   *
   * @since 1.3
   */
  protected int getIndentLevel() {
    return indentLevel;
  }

  /**
   * Does indentation. The number of spaces written
   * out is indent level times the space to map mapping. If the current
   * line is empty, this will not make it so that the current line is
   * still considered empty.
   *
   * @throws IOException on any I/O error
   */
  protected void indent() throws IOException {
    int max = getIndentLevel() * getIndentSpace();
    if (indentChars == null || max > indentChars.length) {
      indentChars = new char[max];
      for (int counter = 0; counter < max; counter++) {
        indentChars[counter] = ' ';
      }
    }
    int length = getCurrentLineLength();
    boolean wasEmpty = isLineEmpty();
    output(indentChars, 0, max);
    if (wasEmpty && length == 0) {
      isLineEmpty = true;
    }
  }

  /**
   * Writes out a character. This is implemented to invoke
   * the <code>write</code> method that takes a char[].
   *
   * @param ch a char.
   * @throws IOException on any I/O error
   */
  protected void write(char ch) throws IOException {
    if (tempChars == null) {
      tempChars = new char[128];
    }
    tempChars[0] = ch;
    write(tempChars, 0, 1);
  }

  /**
   * Writes out a string. This is implemented to invoke the
   * <code>write</code> method that takes a char[].
   *
   * @param content a String.
   * @throws IOException on any I/O error
   */
  protected void write(String content) throws IOException {
    if (content == null) {
      return;
    }
    int size = content.length();
    if (tempChars == null || tempChars.length < size) {
      tempChars = new char[size];
    }
    content.getChars(0, size, tempChars, 0);
    write(tempChars, 0, size);
  }

  /**
   * Writes the line separator. This invokes <code>output</code> directly
   * as well as setting the <code>lineLength</code> to 0.
   *
   * @since 1.3
   */
  protected void writeLineSeparator() throws IOException {
    String newline = getLineSeparator();
    int length = newline.length();
    if (newlineChars == null || newlineChars.length < length) {
      newlineChars = new char[length];
    }
    newline.getChars(0, length, newlineChars, 0);
    output(newlineChars, 0, length);
    setCurrentLineLength(0);
  }

  /**
   * All write methods call into this one. If <code>getCanWrapLines()</code>
   * returns false, this will call <code>output</code> with each sequence
   * of <code>chars</code> that doesn't contain a NEWLINE, followed
   * by a call to <code>writeLineSeparator</code>. On the other hand,
   * if <code>getCanWrapLines()</code> returns true, this will split the
   * string, as necessary, so <code>getLineLength</code> is honored.
   * The only exception is if the current string contains no whitespace,
   * and won't fit in which case the line length will exceed
   * <code>getLineLength</code>.
   *
   * @since 1.3
   */
  protected void write(char[] chars, int startIndex, int length)
      throws IOException {
    if (!getCanWrapLines()) {
      // We can not break string, just track if a newline
      // is in it.
      int lastIndex = startIndex;
      int endIndex = startIndex + length;
      int newlineIndex = indexOf(chars, NEWLINE, startIndex, endIndex);
      while (newlineIndex != -1) {
        if (newlineIndex > lastIndex) {
          output(chars, lastIndex, newlineIndex - lastIndex);
        }
        writeLineSeparator();
        lastIndex = newlineIndex + 1;
        newlineIndex = indexOf(chars, '\n', lastIndex, endIndex);
      }
      if (lastIndex < endIndex) {
        output(chars, lastIndex, endIndex - lastIndex);
      }
    } else {
      // We can break chars if the length exceeds maxLength.
      int lastIndex = startIndex;
      int endIndex = startIndex + length;
      int lineLength = getCurrentLineLength();
      int maxLength = getLineLength();

      while (lastIndex < endIndex) {
        int newlineIndex = indexOf(chars, NEWLINE, lastIndex,
            endIndex);
        boolean needsNewline = false;
        boolean forceNewLine = false;

        lineLength = getCurrentLineLength();
        if (newlineIndex != -1 && (lineLength +
            (newlineIndex - lastIndex)) < maxLength) {
          if (newlineIndex > lastIndex) {
            output(chars, lastIndex, newlineIndex - lastIndex);
          }
          lastIndex = newlineIndex + 1;
          forceNewLine = true;
        } else if (newlineIndex == -1 && (lineLength +
            (endIndex - lastIndex)) < maxLength) {
          if (endIndex > lastIndex) {
            output(chars, lastIndex, endIndex - lastIndex);
          }
          lastIndex = endIndex;
        } else {
          // Need to break chars, find a place to split chars at,
          // from lastIndex to endIndex,
          // or maxLength - lineLength whichever is smaller
          int breakPoint = -1;
          int maxBreak = Math.min(endIndex - lastIndex,
              maxLength - lineLength - 1);
          int counter = 0;
          while (counter < maxBreak) {
            if (Character.isWhitespace(chars[counter +
                lastIndex])) {
              breakPoint = counter;
            }
            counter++;
          }
          if (breakPoint != -1) {
            // Found a place to break at.
            breakPoint += lastIndex + 1;
            output(chars, lastIndex, breakPoint - lastIndex);
            lastIndex = breakPoint;
            needsNewline = true;
          } else {
            // No where good to break.

            // find the next whitespace, or write out the
            // whole string.
            // maxBreak will be negative if current line too
            // long.
            counter = Math.max(0, maxBreak);
            maxBreak = endIndex - lastIndex;
            while (counter < maxBreak) {
              if (Character.isWhitespace(chars[counter +
                  lastIndex])) {
                breakPoint = counter;
                break;
              }
              counter++;
            }
            if (breakPoint == -1) {
              output(chars, lastIndex, endIndex - lastIndex);
              breakPoint = endIndex;
            } else {
              breakPoint += lastIndex;
              if (chars[breakPoint] == NEWLINE) {
                output(chars, lastIndex, breakPoint++ -
                    lastIndex);
                forceNewLine = true;
              } else {
                output(chars, lastIndex, ++breakPoint -
                    lastIndex);
                needsNewline = true;
              }
            }
            lastIndex = breakPoint;
          }
        }
        if (forceNewLine || needsNewline || lastIndex < endIndex) {
          writeLineSeparator();
          if (lastIndex < endIndex || !forceNewLine) {
            indent();
          }
        }
      }
    }
  }

  /**
   * Writes out the set of attributes as " &lt;name&gt;=&lt;value&gt;"
   * pairs. It throws an IOException when encountered.
   *
   * @param attr an AttributeSet.
   * @throws IOException on any I/O error
   */
  protected void writeAttributes(AttributeSet attr) throws IOException {

    Enumeration names = attr.getAttributeNames();
    while (names.hasMoreElements()) {
      Object name = names.nextElement();
      write(" " + name + "=" + attr.getAttribute(name));
    }
  }

  /**
   * The last stop in writing out content. All the write methods eventually
   * make it to this method, which invokes <code>write</code> on the
   * Writer.
   * <p>This method also updates the line length based on
   * <code>length</code>. If this is invoked to output a newline, the
   * current line length will need to be reset as will no longer be
   * valid. If it is up to the caller to do this. Use
   * <code>writeLineSeparator</code> to write out a newline, which will
   * property update the current line length.
   *
   * @since 1.3
   */
  protected void output(char[] content, int start, int length)
      throws IOException {
    getWriter().write(content, start, length);
    setCurrentLineLength(getCurrentLineLength() + length);
  }

  /**
   * Support method to locate an occurrence of a particular character.
   */
  private int indexOf(char[] chars, char sChar, int startIndex,
      int endIndex) {
    while (startIndex < endIndex) {
      if (chars[startIndex] == sChar) {
        return startIndex;
      }
      startIndex++;
    }
    return -1;
  }
}
